﻿using Bouyei.Geo.Converters;
using Bouyei.Geo.Geometries;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Bouyei.Geo.GeoParsers.Shpfile
{
    public class GeometryReader : BaseFile
    {
        BitExtensions bigEndian = null;
        BitExtensions littelEndian = null;

        public GeometryReader(string pathName)
            : base(pathName)
        {
            bigEndian = new BitExtensions(ByteOrder.BigEndian);
            littelEndian = new BitExtensions(ByteOrder.LittleEndian);
        }

        public ShpFile Reader()
        {
            var reader = BinReaderStream();
            ShpHeader header = GetShpHeader(reader);
            int preAllocateCnt = header.Length / 10000;

            ShpFile shpfile = GetShpFeature(preAllocateCnt, header, reader);
            return shpfile;
        }

        public ShpHeader GetShpHeader(BinaryReader reader)
        {
            ShpHeader header = new ShpHeader();

            header.FileCode = bigEndian.ToInt32(reader);
            if (header.FileCode != 9994) throw new Exception("invalid shpfile");

            reader.ReadBytes(20);//跳过保留未用字节

            header.Length = bigEndian.ToInt32(reader);//文件长度
            header.Version = littelEndian.ToInt32(reader);
            int gtype = littelEndian.ToInt32(reader);
            if (gtype == 1)
            {
                header.geometryType = GeoType.POINT;
            }
            else if (gtype == 3)
            {
                header.geometryType = GeoType.LINESTRING;
            }
            else if (gtype == 5)
            {
                header.geometryType = GeoType.POLYGON;
            }

            header.min = new Coordinate(reader.ReadDouble(), reader.ReadDouble());
            header.max = new Coordinate(reader.ReadDouble(), reader.ReadDouble());

            header.z = new Coordinate(reader.ReadDouble(), reader.ReadDouble());
            header.m = new Coordinate(reader.ReadDouble(), reader.ReadDouble());

            return header;
        }

        protected ShpFile GetShpFeature(int geometryCount, ShpHeader header, BinaryReader reader)
        {
            ShpFile feature = new ShpFile(header);
            switch (header.geometryType)
            {
                case GeoType.POINT:
                    {
                        var points = GetPointByBinaryReader(geometryCount, reader);
                        feature.Features = new List<ShpFeature>(points.Count);

                        foreach (var point in points)
                            feature.Features.Add(new ShpFeature()
                            {
                                geometry = new Geometry(point.Point),
                                OID = point.OID
                            });
                    }
                    break;
                case GeoType.LINESTRING:
                    {
                        var lineStrings = GetLineStringByBinaryReader(geometryCount, reader);
                        feature.Features = new List<ShpFeature>(lineStrings.Count);

                        foreach (var linestring in lineStrings)
                            feature.Features.Add(new ShpFeature()
                            {
                                geometry = new Geometry(GeoType.LINESTRING, linestring.coordiantes),
                                OID = linestring.OID
                            });
                    }
                    break;
                case GeoType.POLYGON:
                    {
                        var polygons = GetPolygonByBinaryReader(geometryCount, reader);
                        feature.Features = new List<ShpFeature>(polygons.Count);

                        foreach (var polygon in polygons)
                        {
                            //shell and holes 
                            var ls = ResolveShellHoles(polygon.coordiantes);
                            if (ls.Count == 1)
                            {
                                feature.Features.Add(new ShpFeature()
                                {
                                    geometry = new Geometry(GeoType.POLYGON, polygon.coordiantes),
                                    OID = polygon.OID
                                });
                            }
                            else
                            {
                                var coords = ls.GetRange(1, polygons.Count - 1);
                                feature.Features.Add(new ShpFeature()
                                {
                                    OID = polygon.OID,
                                    geometry = new Geometry(polygon.coordiantes, coords)
                                });
                            }

                            //all to shells
                            //feature.geometries.Add(new Geometry(GeoType.POLYGON, polygon.coordiantes));
                        }
                    }
                    break;
            }
            return feature;
        }

        protected List<ShpPoint> GetPointByBinaryReader(int geometryCount, BinaryReader reader)
        {
            List<ShpPoint> points = new List<ShpPoint>(geometryCount);
            while (reader.PeekChar() != -1)
            {
                ShpPoint point = new ShpPoint();
                point.OID = bigEndian.ToInt32(reader);

                //记录头8字节和4字节地shptype
                reader.ReadBytes(8);

                point.Point = new Coordinate()
                {
                    X = reader.ReadDouble(),
                    Y = reader.ReadDouble()
                };

                points.Add(point);
            }

            return points;
        }

        protected List<ShpPolyLine> GetLineStringByBinaryReader(int geometryCount, BinaryReader reader)
        {
            List<ShpPolyLine> lines = new List<ShpPolyLine>(geometryCount);
            while (reader.PeekChar() != -1)
            {
                ShpPolyLine line = new ShpPolyLine();
                line.OID = bigEndian.ToInt32(reader);

                int DataLength = bigEndian.ToInt32(reader);

                reader.ReadInt32();


                line.Box[0] = reader.ReadDouble();
                line.Box[1] = reader.ReadDouble();
                line.Box[2] = reader.ReadDouble();
                line.Box[3] = reader.ReadDouble();

                line.NumParts = reader.ReadInt32();
                line.NumPoints = reader.ReadInt32();
                if (line.NumPoints == 0) continue;

                line.Parts = new List<int>(line.NumParts);
                line.coordiantes = new Coordinate[line.NumPoints];

                for (int i = 0; i < line.NumParts; ++i)
                {
                    line.Parts.Add(reader.ReadInt32());
                }

                for (int i = 0; i < line.NumPoints; ++i)
                {
                    line.coordiantes[i] = new Coordinate()
                    {
                        X = reader.ReadDouble(),
                        Y = reader.ReadDouble()
                    };
                }

                lines.Add(line);
            }
            return lines;
        }

        protected List<ShpPolygon> GetPolygonByBinaryReader(int geometryCount, BinaryReader reader)
        {
            List<ShpPolygon> polygons = new List<ShpPolygon>(geometryCount);

            while (reader.PeekChar() != -1)
            {
                ShpPolygon polygon = new ShpPolygon();
                polygon.OID = bigEndian.ToInt32(reader);

                int DataLength = bigEndian.ToInt32(reader);

                int m = reader.ReadInt32();

                polygon.Box[0] = reader.ReadDouble();
                polygon.Box[1] = reader.ReadDouble();
                polygon.Box[2] = reader.ReadDouble();
                polygon.Box[3] = reader.ReadDouble();
                polygon.NumParts = reader.ReadInt32();
                polygon.NumPoints = reader.ReadInt32();
                if (polygon.NumPoints == 0) continue;

                polygon.Parts = new List<int>(polygon.NumParts);
                polygon.coordiantes = new Coordinate[polygon.NumPoints];

                for (int i = 0; i < polygon.NumParts; ++i)
                {
                    polygon.Parts.Add(reader.ReadInt32());
                }

                for (int i = 0; i < polygon.NumPoints; ++i)
                {
                    polygon.coordiantes[i] = new Coordinate()
                    {
                        X = reader.ReadDouble(),
                        Y = reader.ReadDouble()
                    };
                }

                polygons.Add(polygon);
            }
            return polygons;
        }

        //resolve polygon the shell and holes
        private List<Coordinate[]> ResolveShellHoles(Coordinate[] coordinates)
        {
            List<Coordinate[]> coords = new List<Coordinate[]>(32);
            Coordinate start = coordinates[0];

            List<Coordinate> tmp = new List<Coordinate>(64);
            tmp.Add(start);
            tmp.Add(coordinates[1]);

            for (int i = 2; i < coordinates.Length; ++i)
            {
                var _coord = coordinates[i];
                if (start.Equals(_coord))
                {
                    tmp.Add(_coord);
                    coords.Add(tmp.ToArray());
                    tmp.Clear();

                    if (i < coordinates.Length - 1)
                    {
                        start = coordinates[i + 1];
                        tmp.Add(start);
                    }
                    i += 1;
                }
                else
                {
                    tmp.Add(_coord);
                }
            }
            return coords;
        }

        public override void Dispose()
        {
            base.Dispose();
        }
    }
}
